Skip to content

Add dynamically generated event invoice with PDF download#1709

Merged
maebeale merged 6 commits into
mainfrom
maebeale/invoice-page-pdf-download
Jun 17, 2026
Merged

Add dynamically generated event invoice with PDF download#1709
maebeale merged 6 commits into
mainfrom
maebeale/invoice-page-pdf-download

Conversation

@maebeale

@maebeale maebeale commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

What is the goal of this PR and why is this important?

  • Provide a printable invoice for paid events that's generated from live data instead of a hand-maintained file per event.
  • The brand logo comes from the asset pipeline, the recipient name from the registration/payer, and the line item (description + price) from the event.
  • Works for both audiences: registrants and bulk-payment payers (public), and AWBW staff (admin).

How did you approach the change?

  • Added an EventInvoice presenter (app/presenters/event_invoice.rb) that normalizes three sources into one printable layout:
    • from_registration — one attendee billed at the event cost; the registrant's snapshotted organization (with address) is the Invoice To, falling back to the person.
    • from_event — a blank template carrying only the event's content.
    • from_bulk_payment — bills the payer's organization for every attendee submitted (qty × event cost).
  • Routes:
    • GET /events/:id/invoiceevents/invoices#show. Renders the blank template (admin-only); autofills from a bulk-payment submission when submission_id is present, and that case is public (gated by FormSubmissionPolicy#show_invoice?) — matching that the bulk-payment submission show page is already public by id.
    • GET /registration/:slug/invoiceevents/registrations#invoice (public, the secret slug is the authorization).
  • PDF download reuses the app's existing convention — a "Download PDF" button with inline onclick="window.print();" (same as the recipients/background/social-share views), and the chrome is print:hidden so only the invoice document lands in the PDF. No PDF gem/binary and no new Stimulus controller.
  • Entry points:
    • Registration ticket → "View invoice" (when the event has a cost)
    • Public bulk-payment submission page (the link payers get in their confirmation email) → "View invoice"
    • Admin form-submission page (bulk-payment role) → "View invoice"
    • Admin bulk-payments page → per-submission "Invoice" links + a "Blank invoice" button
    • Return-aware eyebrow via return_to so each origin gets the correct back link.

UI Testing Checklist

  • Registration ticket for a paid event shows "View invoice" → renders the invoice with registrant/org and event line item
  • "Download PDF" opens the print dialog with only the invoice visible (nav, footer, buttons hidden)
  • Public bulk-payment submission page (signed out) shows "View invoice" → renders the org/attendee invoice
  • Admin bulk-payments page: "Blank invoice" renders the empty template; each submission's "Invoice" autofills org/attention and attendee quantity
  • Non-admin hitting /events/:id/invoice (blank) or a non-bulk submission's invoice is redirected

Anything else to add?

  • Bulk submissions have no structured address (payer/org are free-text form fields), so those invoices show the org name + payer email but no street address; per-registration invoices show the org's real address.
  • Per-registration and bulk-payment invoices are intentionally reachable without an account, consistent with the existing registration ticket and bulk-payment submission pages (both gated by a slug / public-by-id).

🤖 Generated with Claude Code

payer = submission.person
payer_name = [ answers["payer_first_name"], answers["payer_last_name"] ]
.map(&:presence).compact.join(" ").presence || payer&.full_name
quantity = [ submission.bulk_payment_attendee_count, 1 ].max

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Bulk invoices bill the payer's org for every attendee submitted (qty × event cost), so an 8-person submission for a $1,500 event totals $12,000 — matching the source template. Falls back to qty 1 if the count is missing/zero.

def show
authorize! @event, to: :invoice?

if params[:submission_id].present?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: One admin route serves both cases: no submission_id → blank template with just the event content; with one → autofilled from the bulk-payment submission. This is why there's no dedicated bulk-payment invoice route.

end
end

def invoice

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Public invoice authorized via show_public? (the secret slug is the credential), consistent with the registration ticket page right below it. The admin /events/:id/invoice route is the manage-gated counterpart.

def show_invoice?
record.role == "bulk_payment" || admin?
end
end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Bulk-payment submissions are already publicly viewable by id (the payer has no account and is emailed the link), so their invoice follows the same rule. Other submission roles (registration/scholarship) stay admin-only.

# autofills the bill-to/attention from that bulk-payment submission.
class InvoicesController < ApplicationController
# Bulk-payment payers have no account; authorization (below) gates access.
skip_before_action :authenticate_user!, only: [ :show ]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Auth is skipped here (like the bulk-payment controller's public actions) and access is enforced by the per-branch authorize! calls below: public show_invoice? for a bulk submission, admin invoice? for the blank template.

@maebeale maebeale marked this pull request as ready for review June 17, 2026 17:56
maebeale and others added 3 commits June 17, 2026 13:57
AWBW staff and registrants need a printable invoice for paid events.
Rather than maintaining a static template per event, this generates the
invoice from live data — the brand logo from the asset pipeline, the
recipient from the registration/payer, and the line item from the event.

One EventInvoice presenter normalizes three sources into a single
printable layout: a per-registration invoice (public, reached via the
registration's secret slug), and an admin-side event invoice that renders
a blank template prefilled with the event's content, autofilling the
bill-to/attention from a bulk-payment submission when submission_id is
present. PDF export reuses the app's existing print convention (print:
Tailwind utilities + a small print Stimulus controller) so no PDF gem or
binary is introduced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bulk-payment payers are emailed a link to their (public) submission page, so
the invoice needs to be reachable from there — add a "View invoice" link on
both the public bulk-payment show page and the admin form-submission page.
A bulk-payment submission's invoice is now public (gated by FormSubmission
show_invoice?), matching that the submission show page is already public by id;
the blank template stays admin-only.

Also drop the bespoke print Stimulus controller in favor of the inline
onclick="window.print();" pattern already used by sibling event views
(recipients, background, social share), and render the invoice on a neutral
public background rather than the admin blue tint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Main (#1707) added an "Invoice"/"W-9" opt-in to public registration that
sets invoice_requested/w9_requested, with the digital ticket surfacing those
downloads — but only the W-9 download was wired up, since the invoice page
didn't exist yet. Surface the invoice the same way: a card matching the W-9
one, shown when the registrant requested an invoice, completing that intent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@maebeale maebeale force-pushed the maebeale/invoice-page-pdf-download branch from 53f04d6 to 160eb51 Compare June 17, 2026 18:00
maebeale and others added 3 commits June 17, 2026 14:02
Mark two facilitator/trauma training registrations as having requested an
invoice so the dev dataset exercises the ticket's "View invoice" surface,
mirroring the existing scholarship_requested seed flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gives the dev dataset a registration that exercises both the W-9 and invoice
ticket surfaces at once, and threads w9_requested through the seed create.
Maria and Sarah keep their invoice-only flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new event/registration invoice views set content_for(:page_bg_class),
which the alignment guard requires to be accounted for. Both are publicly
reachable (slug / bulk-payment submission id), so "public" is the honest
value; the blank-template admin gating is enforced in the controller.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@maebeale maebeale merged commit 590cd18 into main Jun 17, 2026
3 checks passed
@maebeale maebeale deleted the maebeale/invoice-page-pdf-download branch June 17, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant